﻿// $Id: $
// $DateTime: $
// Copyright © 2008 Tristen Fielding

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace ResXFileCodeGenerator.Internal
{
    internal static class StringFormatValidator
    {
        /// <summary>
        /// Validate the format string by verifying that the 
        /// syntax is correct and that the indices are valid and formatted correctly.
        /// </summary>
        /// <param name="resName">The resource name for this comment.</param>
        /// <param name="format">The format string.</param>
        /// <param name="resStringComments">The string from the comment section.</param>
        /// <remarks>
        /// The definition of a formatted string:
        /// 
        /// The format parameter consists of zero or more runs of text intermixed with 
        /// zero or more indexed placeholders (called format items) that correspond to an 
        /// object in the parameter list of this method.
        /// 
        /// The syntax of a format item is {index[,alignment][:formatString]}, which specifies 
        /// a mandatory index, the optional length and alignment of the formatted text, 
        /// and an optional string of format specifier characters that govern how 
        /// the value of the corresponding object is formatted. 
        /// 
        /// The components of a format item are: 
        /// 
        /// index 
        /// A zero-based integer that indicates which element in a list of objects to format. 
        /// 
        /// alignment 
        /// An optional integer indicating the minimum width of the region to contain the formatted value. 
        /// If the length of the formatted value is less than alignment, then the region is padded with spaces. 
        /// If alignment is negative, the formatted value is left justified in the region; 
        /// if alignment is positive, the formatted value is right justified. 
        /// If alignment is not specified, the length of the region is the length of the formatted value. 
        /// 
        /// The comma is required if alignment is specified. 
        /// 
        /// formatString 
        /// An optional string of format specifiers. 
        /// If formatString is not specified and the corresponding argument implements the IFormattable interface, 
        /// then null is used as the IFormattable.ToString format string. 
        /// 
        /// The leading and trailing brace characters, '{' and '}', are required. 
        /// To specify a single literal brace character in the format string, 
        /// specify two leading or trailing brace characters; that is, "{{" or "}}".
        /// </remarks>
        /// <returns>The number of indices found in the format string.</returns>
        public static int Validate(string resName, string format, string resStringComments)
        {
            if(resName == null || format == null)
            {
                throw new ArgumentNullException(resName == null ? "resName" : "format");
            }

            if (ExcludeFormatValidation(resStringComments))
                return 0;

            List<int> argIds = new List<int>();
            Regex reg = new Regex(formatSyntax, RegexOptions.Compiled);

            MatchCollection matches = reg.Matches(format);

            for (int i = 0; i < matches.Count; i++ )
            {
                GroupCollection group = matches[i].Groups;

                if (group[(int)FormatSyntaxGroup.Index].Success)
                {
                    int index = int.Parse(group[(int) FormatSyntaxGroup.Index].Value, NumberStyles.Integer);
                    if (!argIds.Contains(index))
                        argIds.Add(index);
                }
                else if (group[(int) FormatSyntaxGroup.Error].Success)
                {
                    FormatError(resName, matches, i);
                }
            }

            // Check to make sure we have continuous set of numbers, beginning at zero.
            //
            argIds.Sort();
            for (int id = 0; id < argIds.Count; id++)
            {
                if(id != argIds[id])
                    throw new FormatException(string.Format(CultureInfo.CurrentUICulture,
                        Strings.FormatIndexOutOfRange, resName));
            }

            return argIds.Count;
        }

        /// <summary>
        /// Parse parameter information from a resource's comment.
        /// </summary>
        /// <param name="resName">The resource name for this comment.</param>
        /// <param name="resStringComments">The resource's comment string.</param>
        /// <returns></returns>
        public static ParamInfo[] ParseParamInfo(string resName, string resStringComments)
        {
            List<ParamInfo> paramList = new List<ParamInfo>();

            if (ExcludeFormatValidation(resStringComments))
                return paramList.ToArray();

            if (!string.IsNullOrEmpty(resStringComments))
            {
                Regex reg = new Regex(paramSyntax, RegexOptions.Compiled);

                MatchCollection matches = reg.Matches(resStringComments);

                foreach(Match match in matches)
                {
                    if (match.Groups[(int) ParamSyntaxGroup.Error].Success)
                    {
                        throw new FormatException(string.Format(CultureInfo.CurrentUICulture,
                            Strings.InvalidParamSyntax, resName, match.Groups[(int) ParamSyntaxGroup.Error].Value));
                    }

                    if (match.Groups[(int) ParamSyntaxGroup.Type].Success && 
                        match.Groups[(int) ParamSyntaxGroup.Name].Success)
                    {
                        ParamInfo paramInfo = new ParamInfo
                        {
                            ParamType = ConvertToFundamentalType(match.Groups[(int) ParamSyntaxGroup.Type].Value),
                            ParamName = match.Groups[(int) ParamSyntaxGroup.Name].Value
                        };

                        if (match.Groups[(int) ParamSyntaxGroup.XmlComment].Success)
                            paramInfo.ParamComments = match.Groups[(int) ParamSyntaxGroup.XmlComment].Value;

                        paramList.Add(paramInfo);
                    }
                    else if (
                        !match.Groups[(int) ParamSyntaxGroup.End].Success &&
                        !match.Groups[(int) ParamSyntaxGroup.Comment].Success) 
                    {
                        throw new Exception("Internal error in StringFormatValidator.ParseParamInfo while parsing:\n"
                            + resStringComments);
                    }
                }
            }

            return paramList.ToArray();
        }

        private static Type ConvertToFundamentalType(string typeName)
        {
            // Note: For the purpose of formatted strings
            // we decided that 'object' is not considered a valid fundamental.
            // Otherwise all inputs would default to object and we wanted to
            // convey better type information to the developer.
            //

            switch(typeName.ToLower())
            {
            case "string":
                return typeof(string);

            case "int":
                return typeof(int);

            case "long":
                return typeof(long);

            case "bool":
                return typeof(bool);

            case "char":
                return typeof(char);

            case "byte":
                return typeof(byte);

            case "float":
                return typeof(float);

            case "double":
                return typeof(double);

            case "decimal":
                return typeof(decimal);

            case "short":
                return typeof(short);

            case "sbyte":
                return typeof(sbyte);

            case "ushort":
                return typeof(ushort);

            case "uint":
                return typeof(uint);

            case "ulong":
                return typeof(ulong);
            }

            throw new FormatException(string.Format(CultureInfo.CurrentUICulture,
                Strings.InvalidFormatStringType, typeName));
        }

        private static void FormatError(string resName, MatchCollection matches, int i)
        {
            GroupCollection group = matches[i].Groups;
            StringBuilder errorMessage = new StringBuilder();
                    
            errorMessage.AppendFormat(CultureInfo.CurrentUICulture,
                Strings.InvalidFormatString, resName, group[(int) FormatSyntaxGroup.Error].Index);

            StringBuilder contextHeader = new StringBuilder(Strings.InvalidFormatString_Header);
            StringBuilder context = new StringBuilder();
            bool hasContext = false;

            if (i > 0)
            {
                context.Append("\nBefore: ");
                context.Append(matches[i - 1].Value);
                contextHeader.Append(matches[i - 1].Value);
                hasContext = true;
            }

            context.Append("\nError: ");
            context.Append(matches[i].Value);
            contextHeader.Append(matches[i].Value);

            if (i < matches.Count)
            {
                context.Append("\nAfter: ");
                context.Append(matches[i + 1].Value);
                contextHeader.Append(matches[i + 1].Value);
                hasContext = true;
            }

            if (hasContext)
            {
                errorMessage.Append(contextHeader);
                errorMessage.Append(context);
            }
            else
                errorMessage.Append(".");

            throw new FormatException(errorMessage.ToString());
        }

        private static bool ExcludeFormatValidation(string resStringComments)
        {
            Regex reg = new Regex(excludeFormatValidation, RegexOptions.Compiled | RegexOptions.IgnoreCase);
            
            Match match = reg.Match(resStringComments);

            return match.Success;
        }

        private const string excludeFormatValidation = @"\$exclude\$";

        // Format Syntax:
        // anything that isn't a { or a } is good or
        // {{ or }} are good or
        // {<formatIndex>} or {<formatIndex>,<alignment>} or 
        // {<formatIndex>,<alignment>:<formatString>} or {<formatIndex>:<formatString>} are good but
        // { or } by themselves is an error.
        private const string formatSyntax = @"[^{}]+|{{|}}|(?<!{){(?<Index>\d+)(?:,[+-]?\d+)?(?::[^{}]+)?}|(?<Error>[{}])";

        private enum FormatSyntaxGroup
        {
            Index = 1,
            Error,
        }

        // Param Syntax:
        // $exclude$ -- exclude this resource string from format validation
        // <type> <paramName>[, <type> <paramName>] or
        // <type> <paramName1> /// Param Doc Comments for param 1
        // <type> <paramName2> /// Param Doc Comments for param 2
        // <type> <paramName3> // Comments for translators (IOW not included in generator file.
        // Where type is any 'built-in value' type, excluding object.
        private const string paramSyntax = @"\s*(?:(?<End>$)|(?<Comment>//.*)|(?:(?:(?:(?<Type>@?[a-zA-Z][a-zA-Z0-9]*)\s+(?<Name>@?[a-zA-Z_][a-zA-Z0-9_]*))|(?<Error>.*?))\s*(?:,|\n|\r|(?:///(?<xmlComment>.*))|(?://.*)|\s*$)))";

        private enum ParamSyntaxGroup
        {
            End = 1,
            Comment,
            Type,
            Name,
            Error,
            XmlComment,
        }
    }
}